package com.androidserver.util;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;

/**
 * Singleton class for communicating with the Google Datastore Service
 * @author Oliver
 *
 */
public class DatastoreWrapper {
	
	/**
	 * Current instance of the class
	 */
	private static DatastoreWrapper instance;
	/**
	 * Ancestor key for all the members in the datastore
	 */
	public static final Key MEMBERS_KEY = KeyFactory.createKey("Members", "AllMembers");
	/**
	 * Ancestor key for all the groups in the datastore
	 */
	public static final Key GROUPS_KEY = KeyFactory.createKey("Groups", "AllGroups");
	/**
	 * The Google Datastore
	 */
	private DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
	
	/**
	 * Returns the current instance of this class, if not instantiated, creates a new instance
	 * and returns that.
	 * @return An instance of the DatastoreWrapper class
	 */
	public static synchronized DatastoreWrapper getInstance() {
		if (instance == null) {
			instance = new DatastoreWrapper();
		}
		return instance;
	}
	/**
	 * The constructor is private so that no extra instances can be instantiated
	 */
	private DatastoreWrapper() {
		super();
	}
	
	
	public String addMember(String memberName, String password) {
		Entity member = new Entity("Member", MEMBERS_KEY);
		int passwordHash = password.hashCode();
		String idCode =
			new Integer((System.currentTimeMillis() + memberName).hashCode()).toString();
		Query memberQuery =
			new Query("Member", MEMBERS_KEY).addFilter("memberName", FilterOperator.EQUAL, memberName);
		List<Entity> members = datastore.prepare(memberQuery).asList(FetchOptions.Builder.withDefaults());

		if (members.isEmpty()) {
			member.setProperty("memberName", memberName);
			member.setProperty("lastCheckIn", System.currentTimeMillis());
			member.setProperty("latitude", 0);
			member.setProperty("longitude", 0);
			member.setProperty("idCode", idCode);
			member.setProperty("password", passwordHash);
			datastore.put(member);
			return idCode;
		} else return "FAIL";
	}
	
	/**
	 * Removes a member form a group
	 * @param idCode ID code of the member to be removed
	 * @param groupName Name of the group the member is going to be romved from
	 * @return
	 */
	public boolean removeMemberFromGroup(String idCode, String groupName) {
		Entity member = getMemberByID(idCode);
		Entity group = getGroup(groupName);
		if (member != null && group != null) {
			group.removeProperty(idCode);
			datastore.put(group);
			return true;
		} else return false;
	}
	/**
	 * Adds a member to a group
	 * @param member The member to be added
	 * @param group Name Name of the group
	 * @return <b>true</b> if the successful,<br>
	 * <b>false</b> if failure
	 */
	public boolean addMemberToGroup(Entity member, String groupName) {
		
		Query groupQuery =
			new Query("Group", GROUPS_KEY).addFilter("groupName", FilterOperator.EQUAL, groupName);
		List<Entity> groups = datastore.prepare(groupQuery).asList(FetchOptions.Builder.withDefaults());
		
		if (!groups.isEmpty()) {
			String idCode = member.getProperty("idCode").toString();
			groups.get(0).setProperty(idCode, idCode);
			datastore.put(groups.get(0));
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Removes the Entity and any associations with any group for the "Member" Entity
	 * with the provided ID code
	 * @param idCode The ID code of the member to be removed
	 * @return true, if deletion was successful<br>
	 * false, if deletion was unsuccessful (most likely the Entity didn't exist)
	 */
	public boolean removeMember(String idCode) {
		Entity member = getMemberByID(idCode);
		if (member != null) {
			datastore.delete(member.getKey());
			List<Entity> groups = getGroups();
			for (int i = 0; i < groups.size(); i++) {
				List<Entity> groupMembers = getGroupMembers(groups.get(i));
				for (int j = 0; j < groupMembers.size(); j++) {
					if (groupMembers.get(j).getProperty("idCode").toString()
							.equals(member.getProperty("idCode").toString()))
						groups.get(i).removeProperty(member.getProperty("idCode").toString());
				}
				datastore.put(groups.get(i));
			}
			return true;
		} else return false;
	}
	
	public boolean addGroup(Entity group) {
		String groupName = group.getProperty("groupName").toString();
		Query groupQuery =
			new Query("Group", GROUPS_KEY).addFilter("groupName", FilterOperator.EQUAL, groupName);
		List<Entity> groups = datastore.prepare(groupQuery).asList(FetchOptions.Builder.withDefaults());
		
		if (groups.isEmpty()) {
			datastore.put(group);
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Removes the group. The ownerName and ownerID parameters are for validation
	 * (only the owner can remove a group)
	 * @param groupName Name of the group to be removed
	 * @param ownerName Group's owner's name
	 * @param ownerID Group's owner's ID
	 * @return true, if deletion was successful<br>
	 * false, if deletion was unsuccessful (most likely the Entity didn't exist)
	 */
	public boolean removeGroup(String groupName, String ownerID) {
		Query groupQuery = new Query("Group", GROUPS_KEY)
			.addFilter("groupName", FilterOperator.EQUAL, groupName)
			.addFilter("groupOwnerID", FilterOperator.EQUAL, ownerID);
		List<Entity> groups = datastore.prepare(groupQuery).asList(FetchOptions.Builder.withDefaults());
		if (!groups.isEmpty()) {
			datastore.delete(groups.get(0).getKey());
			return true;
		} else return false;
	}
	
	public boolean unjoinGroup(String idCode, String groupName) {
		Entity group = getGroup(groupName);
		if (group.hasProperty(idCode)) {
			group.removeProperty(idCode);
			datastore.put(group);
			return true;
		} else return false;
	}
	
	/**
	 * Gets all the "Member" type Entities that are associated with the group
	 * @param group Name of the group
	 * @return A List of "Member" type Entities
	 */
	public List<Entity> getGroupMembers(Entity group) {
		List<Entity> result = new ArrayList<Entity>();
		List<String> memberCodes;
		Query memberQuery = new Query("Member", MEMBERS_KEY);
		Query groupQuery = new Query("Group", GROUPS_KEY)
				.addFilter("groupName", FilterOperator.EQUAL, group.getProperty("groupName").toString());
		List<Entity> members = datastore.prepare(memberQuery).asList(FetchOptions.Builder.withDefaults());
		List<Entity> groups = datastore.prepare(groupQuery).asList(FetchOptions.Builder.withDefaults());
		if (groups.size() != 0) {
			memberCodes = getMemberCodes(groups.get(0));
			for (int i = 0; i < members.size(); i++) {
				if (memberCodes.contains(members.get(i).getProperty("idCode").toString())) {
					result.add(members.get(i));
				}
			}
			return result;
		}
		return null;
	}
	
	/**
	 * Gets all the ID codes of the members that are associated with the group
	 * @param group Name of the group
	 * @return A List of ID codes
	 */
	private List<String> getMemberCodes(Entity group) {
		String tempCode;
		List<String> result = new ArrayList<String>();
		Iterator<String> i = group.getProperties().keySet().iterator();
		while (i.hasNext()) {
			tempCode = i.next();
			if (!tempCode.equalsIgnoreCase("groupName")
					|| !tempCode.equalsIgnoreCase("groupOwnerId")) result.add(tempCode);
		}
		return result;
	}
	
	/**
	 * @param group Name of the group
	 * @return Name of the owner of the group
	 */
	public String getOwnerName(Entity group) {
		Query groupQuery = new Query("Group", GROUPS_KEY)
			.addFilter("groupName", FilterOperator.EQUAL, group.getProperty("groupName"));
		List<Entity> groups = datastore.prepare(groupQuery).asList(FetchOptions.Builder.withDefaults());
		if (!groups.isEmpty())
			return groups.get(0).getProperty("groupOwnerName").toString();
		else return null;
	}
	
	/**
	 * @param group Name of the group
	 * @return ID of the owner of the group
	 */
	public String getOwnerID(Entity group) {
		Query groupQuery = new Query("Group", GROUPS_KEY)
			.addFilter("groupName", FilterOperator.EQUAL, group.getProperty("groupName"));
		List<Entity> groups = datastore.prepare(groupQuery).asList(FetchOptions.Builder.withDefaults());
		if (!groups.isEmpty())
			return groups.get(0).getProperty("groupOwnerID").toString();
		else return "FAIL";
	}
	
	/**
	 * @param name Name of the group
	 * @return "Group" type Entity with the provided name
	 */
	public Entity getGroup(String name) {
		Query groupQuery = new Query("Group", GROUPS_KEY)
			.addFilter("groupName", FilterOperator.EQUAL, name);
		List<Entity> groups = datastore.prepare(groupQuery).asList(FetchOptions.Builder.withDefaults());
		if (!groups.isEmpty())
			return groups.get(0);
		else return null;
	}
	
	/**
	 * @return A List of all the "Group" type Entities in the datastore
	 */
	public List<Entity> getGroups() {
		Query groupQuery = new Query("Group", GROUPS_KEY);
		return datastore.prepare(groupQuery).asList(FetchOptions.Builder.withDefaults());
	}
	
	/**
	 * @param memberID ID of the member
	 * @return A List of all the "Group" type Entities that the member with the provided ID code
	 * is part of
	 */
	public List<Entity> getGroupsByMember(String memberID) {
		Entity member = getMemberByID(memberID);
		List<Entity> result = new ArrayList<Entity>();
		List<Entity> groups = getGroups();
		List<String> memberCodes;
		for (int i = 0; i < groups.size(); i++) {
			memberCodes = getMemberCodes(groups.get(i));
			if (memberCodes.contains(member.getProperty("idCode").toString()))
				result.add(groups.get(i));
		}
		return result;
	}
	
	public List<Entity> getGroupsByOwner(String memberID) {
		List<Entity> result = new ArrayList<Entity>();
		List<Entity> groups = getGroups();
		for (int i = 0; i < groups.size(); i++) {
			if (groups.get(i).getProperty("groupOwnerID").toString().equals(memberID))
				result.add(groups.get(i));
		}
		return result;
	}
	
	/**
	 * @param name Name of the member
	 * @return "Member" type Entity with the provided name
	 */
	public Entity getMemberByName(String name) {
		Query memberQuery = new Query("Member", MEMBERS_KEY)
			.addFilter("memberName", FilterOperator.EQUAL, name);
		List<Entity> members = datastore.prepare(memberQuery).asList(FetchOptions.Builder.withDefaults());
		if (!members.isEmpty())
			return members.get(0);
		else return null;
	}
	
	/**
	 * @param ID ID of the member
	 * @return "Member" type Entity with the provided ID
	 */
	public Entity getMemberByID(String ID) {
		Query memberQuery = new Query("Member", MEMBERS_KEY)
			.addFilter("idCode", FilterOperator.EQUAL, ID);
		List<Entity> members = datastore.prepare(memberQuery).asList(FetchOptions.Builder.withDefaults());
		if (!members.isEmpty())
			return members.get(0);
		else return null;
	}
	
	/**
	 * @return A List of all the "Member" type Entities in the datastore
	 */
	public List<Entity> getMembers() {
		Query groupQuery = new Query("Member", MEMBERS_KEY);
		return datastore.prepare(groupQuery).asList(FetchOptions.Builder.withDefaults());
	}
	
	/**
	 * Updates the members position
	 * @param ID ID of the member to be updated
	 * @param latitude New latitude
	 * @param longitude New longitude
	 * @return true, if update was successful<br>
	 * false, if update was unsuccessful (most likely the member didn't exist)
	 */
	public boolean updateMember(String ID, String latitude, String longitude) {
		Entity member = getMemberByID(ID);
		if (member != null) {
			member.setProperty("latitude", latitude);
			member.setProperty("longitude", longitude);
			member.setProperty("lastCheckIn", System.currentTimeMillis());
			datastore.put(member);
			return true;
		} else return false;
	}
	
	public boolean changeMemberName(String ID, String newName) {
		Entity member = getMemberByID(ID);
		if (member != null) {
			member.setProperty("memberName", newName);
			datastore.put(member);
			return true;
		} else return false;
	}
	
	public boolean changeMemberPassword(String ID, String oldPw, String newPw) {
		Entity member = getMemberByID(ID);
		if (member != null
				&& oldPw.hashCode() == Integer.parseInt(member.getProperty("password").toString())) {
			int newPwHash = newPw.hashCode();
			member.setProperty("password", newPwHash);
			datastore.put(member);
			return true;
		} else return false;
	}
	
	public String validateMember(String memberName, String password) {
		int passwordHash = password.hashCode();
		Query memberQuery =
			new Query("Member", MEMBERS_KEY).addFilter("memberName", FilterOperator.EQUAL, memberName)
			.addFilter("password", FilterOperator.EQUAL, passwordHash);
		List<Entity> members = datastore.prepare(memberQuery).asList(FetchOptions.Builder.withDefaults());
		if (!members.isEmpty()) return members.get(0).getProperty("idCode").toString();
		else return "FAIL";
	}
	
}
